package com.violet.auth.client.util;


import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.violet.common.exception.IdentityInfoExpiredException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtParserBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;


/**
 * 类说明: jwt 工具类
 * 私钥可以从文件中读取也可以从yml配置文件中读取，这里采用yml配置文件配置私钥
 *
 * @author wqf
 * @date 2021/7/12 15:22
 */
@Slf4j
@Component
public class JwtTokenUtil {

    /**
     * 私钥
     */
    @Value("${rsa.private.key:}")
    private String PRIVATE_KEY;
    /**
     * JWT 过期时间
     */
    @Value("${jwt.expiration}")
    private Long TOKEN_EXPIRE_TIME;
    /**
     * jwr 前缀
     */
    @Value("${jwt.separator}")
    private String JWT_SEPARATOR;
    /**
     * Claim 中的用户名
     */
    public static final String CLAM_KEY_USERNAME = "sub";
    /**
     * Claim 中的创建时间
     */
    public static final String CLAM_KEY_CREATED = "created";

    /**
     * 获取转换后的私钥对象
     */
    public SecretKey getSecretKey() {
        return Keys.hmacShaKeyFor(PRIVATE_KEY.getBytes());
    }

    /**
     * 方法描述: 封装jwt头信息
     *
     * @param
     * @return java.util.Map<java.lang.String, java.lang.Object>
     * @author wqf
     * @date 2024/9/6 10:30
     */
    private static Map<String, Object> getHeader() {
        Map<String, Object> header = new HashMap<>();
        header.put("alg", "HS256");
        header.put("typ", "JWT");
        return header;
    }

    /**
     * 方法描述: 生成token
     *
     * @param claims 共享数据信息 不要放敏感信息
     * @return java.lang.String
     * @author wqf
     * @date 2022/9/27 17:54
     */
    public String generateToken(Map<String, Object> claims) {
        // 指定算法
        Date expireTime = new Date(System.currentTimeMillis() + TOKEN_EXPIRE_TIME);
        Map<String, Object> header = getHeader();
        String token = Jwts.builder()
                .setHeader(header)
                //自定义信息载体，可以放用户相关的一些信息
                .setClaims(claims)
                //token过期时间 单位：毫秒
                .setExpiration(expireTime)
                //签名算法
                .signWith(getSecretKey(), SignatureAlgorithm.HS256)
                .setSubject((String) claims.get(CLAM_KEY_USERNAME))
                .compact();
        return JWT_SEPARATOR + token;
    }



    /**
     * token解析
     *
     * @param token 令牌
     * @return 数据声明
     */
    public Claims parseToken(String token) {
        if (StrUtil.isBlank(token) || !token.startsWith(JWT_SEPARATOR)) {
            throw new RuntimeException("invalid token");
        }
        try {
            token = token.replace(JWT_SEPARATOR, "");
            JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder();
            //设置解析的签名算法
            jwtParserBuilder = jwtParserBuilder.setSigningKey(getSecretKey());
            Jws<Claims> claimsJws = jwtParserBuilder.build().parseClaimsJws(token);
            return claimsJws.getBody();
        } catch (UnsupportedJwtException e) {
            throw new UnsupportedJwtException("不支持的 Jwt token！");
        } catch (ExpiredJwtException e) {
            throw new IdentityInfoExpiredException();
        } catch (MalformedJwtException e) {
            throw new MalformedJwtException("token 格式异常！");
        }
    }


    /**
     * 验证 token 是否过期
     *
     * @param token JWT 的 token
     * @return token 是否过期 true：过期 false：未过期
     */
    private boolean isTokenExpired(String token) {
        Date expiredDate = getExpiredDateFromToken(token);
        return expiredDate.before(new Date());
    }

    /**
     * 从 token 中获取过期时间
     *
     * @param token JWT 的 token
     * @return 过期时间
     */
    private Date getExpiredDateFromToken(String token) {
        return parseToken(token).getExpiration();
    }

    /**
     * 判断 token 是否可以被刷新
     *
     * @param token JWT 的 token
     * @return token 是否可以被刷新 true：可以 false：不可以
     */
    private boolean tokenRefreshJustBefore(String token) {
        // 解析 JWT 的 token 拿到负载
        Claims claims = parseToken(token);
        // 获取 token 的创建时间
        Date tokenCreateDate = claims.get(CLAM_KEY_CREATED, Date.class);
        // 获取当前时间
        Date refreshDate = new Date();
        // 条件1: 当前时间在 token 创建时间之后
        // 条件2: 当前时间在（token 创建时间 + 指定时间段）之前（即指定时间段内可以刷新 token）
        return refreshDate.after(tokenCreateDate) && refreshDate.before(DateUtil.offsetSecond(tokenCreateDate, 1800));
    }

    //================ public methods ==================


    /**
     * 从 token 中获取登录用户名
     *
     * @param token JWT 的 token
     * @return 登录用户名
     */
    public String getUserNameFromToken(String token) {
        String username;
        try {
            // 从 token 中获取 JWT 中的负载
            Claims claims = parseToken(token);
            // 从负载中获取用户名
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }


    /**
     * 判断 token 是否可以被刷新
     *
     * @param oldToken JWT 的 token
     * @return token 是否可以被刷新 true：可以 false：不可以
     */
    public String refreshHeadToken(String oldToken) {
        // 失效条件1：token 为 null
        if (StrUtil.isEmpty(oldToken)) return null;
        // 失效条件2：token 格式错误（不包含 "Bearer "）
        String token = oldToken.substring(JWT_SEPARATOR.length());
        if (StrUtil.isEmpty(token)) return null;
        // 失效条件3：token 中没有负载
        Claims claims = parseToken(oldToken);
        if (claims == null) return null;
        // 失效条件4：token 已过期
        if (isTokenExpired(oldToken)) return null;

        // 如果 token 在 30 分钟之内刚刷新过，返回原 token
        if (tokenRefreshJustBefore(oldToken)) {
            return oldToken;
        } else { // 否则，生成新的 token
            // 设置负载中的创建时间
            claims.put(CLAM_KEY_CREATED, new Date());
            // 根据负载生成 token
            return generateToken(claims);
        }
    }

}